ActionHandler.switchVote   C
last analyzed

Complexity

Conditions 11

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 36
rs 5.4
c 0
b 0
f 0
cc 11

How to fix   Complexity   

Complexity

Complex classes like ActionHandler.switchVote often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import {chat_v1 as chatV1} from '@googleapis/chat';
2
import BaseHandler from './BaseHandler';
3
import NewPollFormCard from '../cards/NewPollFormCard';
4
import {addOptionToState, getConfigFromInput, getStateFromCard, getStateFromMessageId} from '../helpers/state';
5
import {callMessageApi} from '../helpers/api';
6
import {createDialogActionResponse, createStatusActionResponse} from '../helpers/response';
7
import PollCard from '../cards/PollCard';
8
import {ClosableType, MessageDialogConfig, PollFormInputs, PollState, Voter} from '../helpers/interfaces';
9
import AddOptionFormCard from '../cards/AddOptionFormCard';
10
import {saveVotes} from '../helpers/vote';
11
import {PROHIBITED_ICON_URL} from '../config/default';
12
import ClosePollFormCard from '../cards/ClosePollFormCard';
13
import MessageDialogCard from '../cards/MessageDialogCard';
14
import {createAutoCloseTask} from '../helpers/task';
15
import ScheduleClosePollFormCard from '../cards/ScheduleClosePollFormCard';
16
import PollDialogCard from '../cards/PollDialogCard';
17
18
/*
19
This list methods are used in the poll chat message
20
 */
21
interface PollAction {
22
  saveOption(): Promise<chatV1.Schema$Message>;
23
24
  getEventPollState(): PollState;
25
}
26
27
export default class ActionHandler extends BaseHandler implements PollAction {
28
  async process(): Promise<chatV1.Schema$Message> {
29
    const action = this.event.common?.invokedFunction;
30
    switch (action) {
31
      case 'start_poll':
32
        return await this.startPoll();
33
      case 'vote':
34
        return this.recordVote();
35
      case 'switch_vote':
36
        return this.switchVote();
37
      case 'vote_form':
38
        return this.voteForm();
39
      case 'add_option_form':
40
        return this.addOptionForm();
41
      case 'add_option':
42
        return await this.saveOption();
43
      case 'show_form':
44
        const pollForm = new NewPollFormCard({topic: '', choices: []}, this.getUserTimezone()).create();
45
        return createDialogActionResponse(pollForm);
46
      case 'new_poll_on_change':
47
        return this.newPollOnChange();
48
      case 'close_poll_form':
49
        return this.closePollForm();
50
      case 'close_poll':
51
        return await this.closePoll();
52
      case 'schedule_close_poll_form':
53
        return this.scheduleClosePollForm();
54
      case 'schedule_close_poll':
55
        return this.scheduleClosePoll();
56
      default:
57
        return createStatusActionResponse('Unknown action!', 'UNKNOWN');
58
    }
59
  }
60
61
  /**
62
   * Handle the custom start_poll action.
63
   *
64
   * @returns {object} Response to send back to Chat
65
   */
66
  async startPoll(): Promise<chatV1.Schema$Message> {
67
    // Get the form values
68
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
69
    const config = getConfigFromInput(formValues);
70
71
    if (!config.topic || config.choices.length === 0) {
72
      // Incomplete form submitted, rerender
73
      const dialog = new NewPollFormCard(config, this.getUserTimezone()).create();
74
      return createDialogActionResponse(dialog);
75
    }
76
77
    if (config.closedTime) {
78
      // because previously in the form, we marked up the time with user timezone offset
79
      const utcClosedTime = config.closedTime - this.getUserTimezone().offset;
80
      if (utcClosedTime < Date.now() - 360000) {
81
        const dialog = new NewPollFormCard(config, this.getUserTimezone()).create();
82
        return createDialogActionResponse(dialog);
83
      }
84
      config.closedTime = utcClosedTime;
85
    }
86
87
    const pollCardMessage = new PollCard({author: this.event.user, ...config},
88
      this.getUserTimezone()).createMessage();
89
    const request = {
90
      parent: this.event.space?.name,
91
      requestBody: pollCardMessage,
92
    };
93
94
    const apiResponse = await callMessageApi('create', request);
95
    if (apiResponse.status === 200 && apiResponse.data?.name) {
96
      await createAutoCloseTask(config, apiResponse.data.name);
97
      return createStatusActionResponse('Poll started.', 'OK');
98
    } else if (apiResponse.status === 444) {
99
      return {
100
        actionResponse: {
101
          type: 'NEW_MESSAGE',
102
        },
103
        ...pollCardMessage,
104
      };
105
    } else {
106
      return createStatusActionResponse('Failed to start poll.', 'UNKNOWN');
107
    }
108
  }
109
110
  /**
111
   * Handle the custom vote action. Updates the state to record
112
   * the user's vote then rerenders the card.
113
   *
114
   * @returns {object} Response to send back to Chat
115
   */
116
  recordVote() {
117
    const parameters = this.event.common?.parameters;
118
    if (!(parameters?.['index'])) {
119
      throw new Error('Index Out of Bounds');
120
    }
121
    const choice = parseInt(parameters['index']);
122
    const userId = this.event.user?.name ?? '';
123
    const userName = this.event.user?.displayName ?? '';
124
    const voter: Voter = {uid: userId, name: userName};
125
    const state = this.getEventPollState();
126
127
    // Add or update the user's selected option
128
    state.votes = saveVotes(choice, voter, state);
129
    const card = new PollCard(state, this.getUserTimezone());
130
    return {
131
      thread: this.event.message?.thread,
132
      actionResponse: {
133
        type: 'UPDATE_MESSAGE',
134
      },
135
      cardsV2: [card.createCardWithId()],
136
    };
137
  }
138
139
  /**
140
   * Handle the custom vote action from poll dialog. Updates the state to record
141
   * the UI will be showed as a dialog
142
   * @param {boolean} eventPollState If true, the event state is from current event instead of calling API to get it
143
   * @returns {object} Response to send back to Chat
144
   */
145
  async switchVote(eventPollState: boolean=false) {
146
    const parameters = this.event.common?.parameters;
147
    if (!(parameters?.['index'])) {
148
      throw new Error('Index Out of Bounds');
149
    }
150
    const choice = parseInt(parameters['index']);
151
    const userId = this.event.user?.name ?? '';
152
    const userName = this.event.user?.displayName ?? '';
153
    const voter: Voter = {uid: userId, name: userName};
154
    let state;
155
    if (!eventPollState && this.event!.message!.name) {
156
      state = await getStateFromMessageId(this.event!.message!.name);
157
    } else {
158
      state = this.getEventPollState();
159
    }
160
161
162
    // Add or update the user's selected option
163
    state.votes = saveVotes(choice, voter, state);
164
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
165
    const request = {
166
      name: this.event!.message!.name,
167
      requestBody: cardMessage,
168
      updateMask: 'cardsV2',
169
    };
170
    callMessageApi('update', request);
171
172
    const card = new PollDialogCard(state, this.getUserTimezone(), voter);
173
    return createDialogActionResponse(card.create());
174
  }
175
176
  /**
177
   * Opens and starts a dialog that allows users to add details about a contact.
178
   *
179
   * @returns {object} open a dialog.
180
   */
181
  addOptionForm() {
182
    const state = this.getEventPollState();
183
    const dialog = new AddOptionFormCard(state).create();
184
    return createDialogActionResponse(dialog);
185
  };
186
187
  /**
188
   * Handle add new option input to the poll state
189
   * the user's vote then rerenders the card.
190
   *
191
   * @returns {object} Response to send back to Chat
192
   */
193
  async saveOption(): Promise<chatV1.Schema$Message> {
194
    const userName = this.event.user?.displayName ?? '';
195
    const state = this.getEventPollState();
196
    const formValues = this.event.common?.formInputs;
197
    const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() ?? '';
198
    addOptionToState(optionValue, state, userName);
199
200
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
201
202
    const request = {
203
      name: this.event.message!.name,
204
      requestBody: cardMessage,
205
      updateMask: 'cardsV2',
206
    };
207
    const apiResponse = await callMessageApi('update', request);
208
    if (apiResponse.status === 200) {
209
      return createStatusActionResponse('Option is added', 'OK');
210
    } else if (apiResponse.status === 444) {
211
      return {
212
        thread: this.event.message?.thread,
213
        actionResponse: {
214
          type: 'UPDATE_MESSAGE',
215
        },
216
        ...cardMessage,
217
      };
218
    } else {
219
      return createStatusActionResponse('Failed to add option.', 'UNKNOWN');
220
    }
221
  }
222
223
  getEventPollState(): PollState {
224
    const stateJson = getStateFromCard(this.event);
225
    if (!stateJson) {
226
      throw new ReferenceError('no valid state in the event');
227
    }
228
    return JSON.parse(stateJson);
229
  }
230
231
  async closePoll(): Promise<chatV1.Schema$Message> {
232
    const state = this.getEventPollState();
233
    state.closedTime = Date.now();
234
    state.closedBy = this.event.user?.displayName ?? '';
235
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
236
    const request = {
237
      name: this.event.message!.name,
238
      requestBody: cardMessage,
239
      updateMask: 'cardsV2',
240
    };
241
242
    if (state.type !== ClosableType.CLOSEABLE_BY_ANYONE && state.author!.name !== this.event.user?.name) {
243
      return createStatusActionResponse('This poll can not be closed by you', 'PERMISSION_DENIED');
244
    }
245
246
    const apiResponse = await callMessageApi('update', request);
247
    if (apiResponse.status === 200) {
248
      return createStatusActionResponse('Poll is closed', 'OK');
249
    } else if (apiResponse.status === 444) {
250
      return {
251
        thread: this.event.message?.thread,
252
        actionResponse: {
253
          type: 'UPDATE_MESSAGE',
254
        },
255
        ...cardMessage,
256
      };
257
    } else {
258
      return createStatusActionResponse('Failed to close poll.', 'UNKNOWN');
259
    }
260
  }
261
262
  closePollForm() {
263
    const state = this.getEventPollState();
264
    if (state.type === ClosableType.CLOSEABLE_BY_ANYONE || state.author!.name === this.event.user?.name) {
265
      return createDialogActionResponse(new ClosePollFormCard(state, this.getUserTimezone()).create());
266
    }
267
268
    const dialogConfig: MessageDialogConfig = {
269
      title: 'Sorry, you can not close this poll',
270
      message: `The poll setting restricts the ability to close the poll to only the creator(${state.author!.displayName}).`,
271
      imageUrl: PROHIBITED_ICON_URL,
272
    };
273
    return createDialogActionResponse(new MessageDialogCard(dialogConfig).create());
274
  }
275
276
  async scheduleClosePoll(): Promise<chatV1.Schema$Message> {
277
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
278
    const config = getConfigFromInput(formValues);
279
280
    // because previously in the form, we marked up the time with user timezone offset
281
    const utcClosedTime = config.closedTime! - this.getUserTimezone()?.offset;
282
    if (utcClosedTime < Date.now() - 360000) {
283
      const dialog = new ScheduleClosePollFormCard(config, this.getUserTimezone()).create();
284
      return createDialogActionResponse(dialog);
285
    }
286
    config.closedTime = utcClosedTime;
287
    const messageId = this.event.message!.name!;
288
    config.autoClose = true;
289
    await createAutoCloseTask(config, messageId);
290
291
    const state = this.getEventPollState();
292
    state.closedTime = utcClosedTime;
293
    const cardMessage = new PollCard(state, this.getUserTimezone()).createMessage();
294
295
    const request = {
296
      name: this.event.message!.name,
297
      requestBody: cardMessage,
298
      updateMask: 'cardsV2',
299
    };
300
    const apiResponse = await callMessageApi('update', request);
301
    if (apiResponse.status === 200) {
302
      return createStatusActionResponse('Poll is scheduled to close', 'OK');
303
    } else if (apiResponse.status === 444) {
304
      return createStatusActionResponse('Your admin need allow you using 3rd party application', 'UNKNOWN');
305
    } else {
306
      return createStatusActionResponse('Failed to schedule close poll. Unknown reason', 'UNKNOWN');
307
    }
308
  }
309
310
  scheduleClosePollForm() {
311
    const state = this.getEventPollState();
312
    return createDialogActionResponse(new ScheduleClosePollFormCard(state, this.getUserTimezone()).create());
313
  }
314
315
  async voteForm() {
316
    return await this.switchVote(true);
317
  }
318
319
  newPollOnChange() {
320
    const formValues: PollFormInputs = this.event.common!.formInputs! as PollFormInputs;
321
    const config = getConfigFromInput(formValues);
322
    return createDialogActionResponse(new NewPollFormCard(config, this.getUserTimezone()).create());
323
  }
324
}
325